/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.huawei.spare.part.replace.views.replace.perform;

import com.huawei.spare.part.replace.Commands;
import com.huawei.spare.part.replace.OperationException;
import com.huawei.spare.part.replace.framework.SmartContentPresenter;
import com.huawei.spare.part.replace.framework.SmartDataBus;
import com.huawei.spare.part.replace.framework.SmartRouter;
import com.huawei.spare.part.replace.models.Configs;
import com.huawei.spare.part.replace.models.PerformTask;
import com.huawei.spare.part.replace.models.Project;
import com.huawei.spare.part.replace.models.Replacement;
import com.huawei.spare.part.replace.rx.RetryWithDelay;
import com.huawei.spare.part.replace.utils.SSHHelper;
import com.huawei.spare.part.replace.views.widgets.Alerts;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.ObservableTransformer;
import io.reactivex.functions.Consumer;
import io.reactivex.internal.functions.Functions;
import io.reactivex.rxjavafx.observables.JavaFxObservable;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import io.reactivex.schedulers.Schedulers;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.util.StringConverter;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.sftp.SFTPClient;
import org.controlsfx.control.PopOver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.huawei.spare.part.replace.Constants.SwitchExportConfig;

/**
 * @author Woo
 */
public class PerformPresenter extends SmartContentPresenter {

  private static final Logger log = LoggerFactory.getLogger(SmartContentPresenter.class);

  // task names
  public static final String BACKUP_CONFIG = "backup-config";
  public static final String PULL_OUT = "pull-out";
  public static final String INSERT = "insert";
  public static final String POWER_OFF = "power-off";
  public static final String RESTORE_UPLINK = "restore-uplink";

  Consumer<Object> ignore = Functions.emptyConsumer();

  @FXML Button performButton;
  @FXML TableView table;
  @FXML TableColumn<PerformTask, String> indexColumn;
  @FXML TableColumn<PerformTask, String> stepColumn;
  @FXML TableColumn<PerformTask, PerformTask.Status> statusColumn;
  @FXML TableColumn<PerformTask, Void> buttonsColumn;

  PopOver popOver;

  ObservableList<PerformTask> tasks = FXCollections.observableArrayList();
  PerformTask backup;
  PerformTask pullout;
  PerformTask insert;
  PerformTask poweroff;
  PerformTask restore;

  SSHClient ssh;
  Configs configs;
  Replacement.Slot slot;

  @Override
  public void initialize() {
    SmartRouter.get().nextStepButton().setDisable(true);
    //final String s = JsonMapper.nonEmptyMapper().toJson(SmartDataBus.get().replacement());
    //System.out.println(s);
    Platform.runLater(() -> {
      initDebugReplacement(); // TODO debug purpose

      configs = SmartDataBus.get().getConfigs();
      slot = SmartDataBus.get().replacement().getSlot();

      initialTasks();
      initialTable();
      initialTaskTrigger();
    });
  }

  private void initialTasks() {
    final Project.Defective defective = SmartDataBus.get().replacement().getProject().getDefective();
    final Project.Spare spare = SmartDataBus.get().replacement().getProject().getSpare();
    final Replacement.Slot slot = SmartDataBus.get().replacement().getSlot();
    this.backup = new PerformTask(1, BACKUP_CONFIG, getString("preform.step.1"));
    this.backup.setObservable(getBackupObservable(backup));

    final String pulloutTitle = String.format(getString("preform.step.2"), slot.getPos(), defective.getBoardType());
    this.pullout = new PerformTask(2, PULL_OUT, pulloutTitle);
    this.pullout.setObservable(getPulloutObservable(pullout));

    final String insertTitle = String.format(getString("preform.step.3"), spare.getBoardType(), slot.getPos());
    this.insert = new PerformTask(3, INSERT, insertTitle);
    this.insert.setObservable(getInsertObservable(insert));

    final String powerOffTitle = String.format(getString("preform.step.4"), spare.getBoardType(), slot.getPos());
    this.poweroff = new PerformTask(4, POWER_OFF, powerOffTitle);
    this.poweroff.setObservable(getPowerOffObservable(poweroff));

    this.restore = new PerformTask(5, RESTORE_UPLINK, getString("preform.step.5"));
    this.restore.setObservable(null);
    tasks.addAll(backup, pullout, insert, poweroff, restore);
  }

  private void initialTaskTrigger() {
    backup.statusProperty().addListener((observable, oldValue, status) -> {
      if (status.equals(PerformTask.Status.Completed)) {
        backup.dispose();
        pullout.run(ignore, ignore);
      }
    });

    pullout.statusProperty().addListener((observable, oldValue, newValue) -> {
      if (newValue.equals(PerformTask.Status.Completed)) {
        pullout.dispose();
        insert.run(ignore, ignore);
      }
    });

    insert.statusProperty().addListener((observable, oldValue, newValue) -> {
      if (newValue.equals(PerformTask.Status.Completed)) {
        insert.dispose();
        poweroff.run(ignore, ignore);
      }
    });

    poweroff.statusProperty().addListener((observable, oldValue, newValue) -> {
      if (newValue.equals(PerformTask.Status.Completed)) {
        poweroff.dispose();
        log.info("[{}] Start task", restore.nameProperty().get());
        table.getSelectionModel().select(tasks.indexOf(restore));
        restore.statusProperty().set(PerformTask.Status.Waiting);
      }
    });

    // 允许进行下一步
    restore.statusProperty().addListener((observable, oldValue, newValue) -> {
      if (newValue.equals(PerformTask.Status.Completed)) {
        log.info("[{}] Execute task succeed", restore.nameProperty().get());
        SmartRouter.get().nextStepButton().setDisable(false);
      }
    });
  }

  private void initialTable() {
    indexColumn.setCellValueFactory(new PropertyValueFactory("index"));
    stepColumn.setCellValueFactory(new PropertyValueFactory("step"));
    statusColumn.setCellValueFactory(new PropertyValueFactory("status"));

    indexColumn.setCellFactory(TextFieldTableCell.forTableColumn());
    stepColumn.setCellFactory(TextFieldTableCell.forTableColumn());
    statusColumn.setCellFactory(TextFieldTableCell.forTableColumn(new StringConverter<PerformTask.Status>() {
      @Override
      public String toString(PerformTask.Status status) {
        return status != null ? getString(status.getDisplay()) : "";
      }

      @Override
      public PerformTask.Status fromString(String string) {
        return null;
      }
    }));

    buttonsColumn.setCellFactory(param -> new TableCell<PerformTask, Void>() {
      private final Button manual = new Button(getString("button.manual"));
      private final Button view = new Button(getString("button.view"));
      private final HBox actionBox = new HBox();

      {
        manual.setManaged(false);
        actionBox.getStyleClass().add("action-box");
        actionBox.getChildren().add(manual);
        JavaFxObservable.eventsOf(manual, MouseEvent.MOUSE_CLICKED)
            .throttleFirst(500, TimeUnit.MILLISECONDS)
            .subscribe((e) -> {
              PerformTask task = getTableView().getItems().get(getIndex());
              if (task.statusProperty().isNotEqualTo(PerformTask.Status.Completed).get()) {
                if (task.nameProperty().isEqualTo(INSERT).get()) {
                  final Optional<ButtonType> confirm =
                      Alerts.confirm(task.stepProperty().get(), "preform.manual.insert.confirmed.content");
                  if (ButtonType.OK.equals(confirm.get())) {
                    task.run(ignore, ignore);
                  }
                } else {
                  final Optional<ButtonType> confirm =
                      Alerts.confirm(task.stepProperty().get(), "preform.manual.handled.content");
                  if (ButtonType.OK.equals(confirm.get())) {
                    manual.setDisable(true);
                    manual.setVisible(false);
                    manual.setManaged(false);
                    task.statusProperty().set(PerformTask.Status.Completed);
                  }
                }
              }
            });

        JavaFxObservable.eventsOf(view, MouseEvent.MOUSE_CLICKED)
            .throttleFirst(500, TimeUnit.MILLISECONDS)
            .subscribe((e) -> {
              showUplinkTable(view);
            });
      }

      @Override
      protected void updateItem(Void item, boolean empty) {
        super.updateItem(item, empty);
        if (!empty) {
          PerformTask task = getTableView().getItems().get(getIndex());
          final boolean isRestoreTask = task.nameProperty().isEqualTo(RESTORE_UPLINK).get();
          final boolean isPulloutTask = task.nameProperty().isEqualTo(PULL_OUT).get();
          if (isRestoreTask) {
            actionBox.getChildren().add(view);
          }

          task.statusProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue == PerformTask.Status.Failed
                || (isRestoreTask && newValue == PerformTask.Status.Waiting)
                || (isPulloutTask && newValue == PerformTask.Status.Waiting)) {
              manual.setVisible(true);
              manual.setManaged(true);
            } else {
              manual.setVisible(false);
              manual.setManaged(false);
            }
          });
        }
        setGraphic(empty ? null : actionBox);
      }
    });

    table.setItems(tasks);
  }

  /**
   * 显示面板Uplink口连接记录
   */
  private void showUplinkTable(Button source) {
    if (popOver == null) {
      PopupUplinkView popup = new PopupUplinkView();
      popOver = new PopOver(popup.getView());
      popOver.setArrowLocation(PopOver.ArrowLocation.RIGHT_CENTER);
      popOver.setArrowSize(8);
      popOver.setArrowIndent(28);
    }

    if (!popOver.isShowing()) {
      popOver.show(source);
    }
  }

  /**
   * 开始更换
   */
  public void perform(ActionEvent actionEvent) {
    log.info("Start perform tasks now");

    loading("common.message.smm.logging", false);
    Completable.create((emitter) -> {
      log.info("Initialize new SSH client");
      try {
        getSshClient();
        emitter.onComplete();
      } catch (Exception e) {
        emitter.onError(e);
      }
    }).subscribeOn(Schedulers.io())
        .observeOn(JavaFxScheduler.platform())
        .subscribe(() -> {
          this.dismissLoading();
          performButton.setDisable(true);
          getBackupObservable(backup).subscribe(ignore, ignore);
        }, error -> {
          this.dismissLoading();
          log.warn("failed to create ssh client", error);
          final String content = String.format(getString("perform.ssh.connect.failed.content"), error.getMessage());
          Alerts.operationFailed(new OperationException("perform.ssh.connect.failed.title", content));
        });
  }

  private SSHClient getSshClient() throws IOException {
    if (ssh == null || !ssh.isConnected()) {
      final Replacement.SmmAuth auth = SmartDataBus.get().replacement().getSmmAuth();
      ssh = SSHHelper.newSSHClient(auth.getSmmHost(), auth.getSmmUsername(), auth.getSmmPassword());
    }
    return ssh;
  }

  /**
   * 服务器进行下电操作
   */
  private Observable<Object> getPowerOffObservable(final PerformTask task) {
    final Replacement.Slot slot = SmartDataBus.get().replacement().getSlot();
    final RetryWithDelay powerOffSwitchRetryHandler =
        new RetryWithDelay(3, 2000,
            "[%d] Power-off Switch failed, reason: %s");
    return Observable.create((emitter) -> {
      log.info("try to power off switch");
      Commands.CommandResult result = Commands.powerOffSwitch(getSshClient(), slot.getPos(), 1);
      if (!result.isSuccess()) {
        result = Commands.powerOffSwitch(getSshClient(), slot.getPos(), 2);
      }
      if (result.isSuccess()) {
        emitter.onNext(true);
      } else {
        emitter.onError(
            new OperationException("perform.cmd.power.off.failed.title", result.getResult().group()));
      }
    }).doOnSubscribe((d) -> task.statusProperty().set(PerformTask.Status.Processing))
        .retryWhen(powerOffSwitchRetryHandler)
        .compose(taskCompose(task));
  }

  /**
   * 插入单板检测
   */
  private Observable<Object> getInsertObservable(final PerformTask task) {
    final Replacement.Slot slot = SmartDataBus.get().replacement().getSlot();
    final RetryWithDelay handler =
        new RetryWithDelay(configs.getSwitchPresentQueryTimes(), configs.getSwitchPresentQueryDelay(),
            "[%d] Switch is not inserted, reason: %s");
    return Observable.create(emitter -> {
      try {
        final boolean present = slot.isPresent(getSshClient());
        if (present) {
          emitter.onNext(true);
        } else {
          emitter.onError(new OperationException(task.stepProperty().get(), "perform.switch.not.inserted.content"));
        }
      } catch (IOException e) {
        emitter.onError(e);
      }
    }).retryWhen(handler).compose(taskCompose(task));
  }

  /**
   * 拔出单板检测
   */
  private Observable<Object> getPulloutObservable(final PerformTask task) {
    final Replacement.Slot slot = SmartDataBus.get().replacement().getSlot();
    final RetryWithDelay handler =
        new RetryWithDelay(configs.getSwitchPresentQueryTimes(), configs.getSwitchPresentQueryDelay(),
            "[%d] Switch is not pull out, reason: %s");
    return Observable.create(emitter -> {
      try {
        final boolean present = slot.isPresent(getSshClient());
        if (present) {
          emitter.onError(new RuntimeException("Switch is still present"));
        } else {
          emitter.onNext(true);
        }
      } catch (IOException e) {
        emitter.onError(e);
      }
    }).retryWhen(handler).compose(taskCompose(task));
  }

  /**
   * 备份单板配置
   */
  private Observable getBackupObservable(final PerformTask task) {
    final RetryWithDelay exportConfigRetryHandler =
        new RetryWithDelay(configs.getSwitchPresentQueryTimes(), configs.getSwitchPresentQueryDelay(),
            "[%d] Export Switch configs failed, reason: %s");
    return Observable.create(emitter -> {
      final Replacement replacement = SmartDataBus.get().replacement();
      final Replacement.Slot slot = replacement.getSlot();
      String exportConfigCommand = String.format(SwitchExportConfig, slot.getPos());
      try {
        try (Session session = getSshClient().startSession()) {
          Session.Command command = session.exec(exportConfigCommand);
          String result = IOUtils.readFully(command.getInputStream()).toString();
          log.info("Run export command: {} -> {}", exportConfigCommand, result);
          boolean success = result.indexOf("Successed") >= 0;
          if (!success) {
            emitter.onError(new OperationException("perform.cmd.export.config.failed.title", result));
          }
        }

        try (final SFTPClient sftp = getSshClient().newSFTPClient()) {
          final String source = String.format("./exchange/swi%d/swi%d.tar.gz", slot.getPos(), slot.getPos());
          final String exportConfigFilePath = replacement.getExportConfigFilePath();
          log.info("Sftp download to: {}", exportConfigFilePath);
          sftp.get(source, exportConfigFilePath);
        }
        emitter.onNext(true);
      } catch (IOException e) {
        emitter.onError(
            new OperationException("perform.cmd.export.config.failed.title", "perform.cmd.io.exception.content"));
      }
    })
        .doOnSubscribe((d) -> task.statusProperty().set(PerformTask.Status.Processing))
        .retryWhen(exportConfigRetryHandler)
        .compose(taskCompose(task));
  }

  /**
   *
   *
   * @param task
   * @return
   */
  public <T> ObservableTransformer<T, T> taskCompose(PerformTask task) {
    return observable -> observable.subscribeOn(Schedulers.io())
        .observeOn(JavaFxScheduler.platform())
        .doOnSubscribe((d) -> {
          table.getSelectionModel().select(tasks.indexOf(task));
          log.info("[{}] Start task", task.nameProperty().get());
          task.statusProperty().set(PerformTask.Status.Waiting);
        })
        .doOnDispose(() -> {
          log.info(String.format("[%s] Cancel task now", task.nameProperty().get()));
        })
        .doOnError((e) -> {
          log.error(String.format("[%s] Execute task failed", task.nameProperty().get()), e);
          task.statusProperty().set(PerformTask.Status.Failed);
          Alerts.error(e);
        })
        .doOnNext((next) -> {
          log.info("[{}] Execute task succeed", task.nameProperty().get());
          task.statusProperty().set(PerformTask.Status.Completed);
        });
  }

  @Override public Observable<Boolean> onLeave() {
    cleanup();
    return Observable.just(true);
  }

  @Override public Observable<Boolean> onFinish() {
    cleanup();
    return super.onFinish();
  }

  /**
   * 释放资源
   */
  private void cleanup() {
    // dispose all not finished tasks
    tasks.forEach(PerformTask::dispose);
    // close ssh client if necessary
    IOUtils.closeQuietly(ssh);
  }
}
